home *** CD-ROM | disk | FTP | other *** search
/ PC World Komputer 2010 April / PCWorld0410.iso / pluginy Firefox / 7684 / 7684.xpi / resources / fmLogin.js < prev    next >
Text File  |  2009-11-20  |  20KB  |  595 lines

  1. /**
  2.  * Copyright (c) 2008, Jose Enrique Bolanos, Jorge Villalobos
  3.  * All rights reserved.
  4.  *
  5.  * Redistribution and use in source and binary forms, with or without
  6.  * modification, are permitted provided that the following conditions are met:
  7.  *
  8.  *  * Redistributions of source code must retain the above copyright notice,
  9.  *    this list of conditions and the following disclaimer.
  10.  *  * Redistributions in binary form must reproduce the above copyright notice,
  11.  *    this list of conditions and the following disclaimer in the documentation
  12.  *    and/or other materials provided with the distribution.
  13.  *  * Neither the name of Jose Enrique Bolanos, Jorge Villalobos nor the names
  14.  *    of its contributors may be used to endorse or promote products derived
  15.  *    from this software without specific prior written permission.
  16.  *
  17.  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  18.  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  19.  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  20.  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
  21.  * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
  22.  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  23.  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
  24.  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
  25.  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  26.  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  27.  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  28.  **/
  29.  
  30. var EXPORTED_SYMBOLS = [];
  31.  
  32. const Cc = Components.classes;
  33. const Ci = Components.interfaces;
  34.  
  35. Components.utils.import("resource://firefm/fmCommon.js");
  36. Components.utils.import("resource://firefm/fmSecret.js");
  37.  
  38. // Observer topic for changed cookies. We use this to detect if we're logged in
  39. // with Last.FM or not.
  40. const TOPIC_COOKIE_CHANGED = "cookie-changed";
  41.  
  42. // Base 64 characters.
  43. const BASE64 =
  44.   "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
  45.  
  46. // The data used to store and retrieve the Last.fm API session key.
  47. const API_SESSION_LOGIN_HOST = "chrome://firefm";
  48. const API_SESSION_LOGIN_REALM = "Last.fm Web Services";
  49.  
  50. // Last.fm URLs.
  51. const URL_BASE = "http://www.last.fm";
  52. const URL_BASE_SSL = "https://www.last.fm";
  53. const URL_EXT_BASE = "http://ext.last.fm";
  54.  
  55. const URL_RPC = URL_EXT_BASE + "/1.0/webclient/xmlrpc.php";
  56. const URL_HANDSHAKE = URL_EXT_BASE + "/1.0/radio/webclient/handshake.php";
  57.  
  58. // Data payloads for different API calls.
  59. const PARAMS_GET_SESSION =
  60.   "<methodCall><methodName>getSession</methodName><params /></methodCall>";
  61. const PARAMS_HANDSHAKE = "?sessionKey=$(SESSION)&user=$(USER)";
  62. const PARAMS_ADJUST = "?lang=en&session=$(SESSION)&url=$(URL)&user=$(USER)";
  63. const PARAMS_PLAYLIST = "?sk=$(SESSION)&fod=true&y=$(TIMESTAMP)";
  64.  
  65. // Regular expression to extract information from the handshake response.
  66. const RE_RESPONSE_HANDSHAKE =
  67.   /^session\=([^\&]+)\&playlist\_url\=([^\&]+)\&subscriber\=([^\&]+)\&base\_url\=([^\&]+)\&base\_path\=([^\&]+)\&$/;
  68.  
  69. // The amount of time to wait to check the logged in state at startup.
  70. const LOGGED_IN_TIMEOUT = 4 * 1000; // 4 seconds.
  71. // The amount of time to wait to show the API notification.
  72. const API_NOTIFICATION_TIMEOUT = 7 * 1000; // 7 seconds.
  73. // The amount of time to wait before doing the getSession call after checking
  74. // the Last.fm cookie.
  75. const GET_SESSION_TIMEOUT = 250; // 250 ms.
  76.  
  77. /**
  78.  * Handles the authentication against the Last.fm API, and the fetching of
  79.  * playlists.
  80.  */
  81. FireFM.Login = {
  82.   // Topic notifications sent from this object.
  83.   get TOPIC_USER_AUTHENTICATION() { return "firefm-user-authentication"; },
  84.  
  85.   /* Home URL. */
  86.   get URL_HOME() { return URL_BASE; },
  87.   /* Login URL. */
  88.   get URL_LOGIN() {
  89.     let url = URL_BASE_SSL + "/login";
  90.  
  91.     // point to the API permission page when doing the first login, or when
  92.     // trying to login and there's already a user logged in.
  93.     if (!this._useNormalLogin || (null != this._userName)) {
  94.       url = this.URL_API_ACCESS;
  95.       this._useNormalLoginPref.value = true;
  96.       this._useNormalLogin = true;
  97.     }
  98.  
  99.     return url;
  100.   },
  101.  
  102.   /* Logout URL. */
  103.   get URL_LOGOUT() { return URL_BASE + "/login/logout"; },
  104.   /* API access approval URL. */
  105.   get URL_API_ACCESS() {
  106.     if (null == this._apiAccessURL) {
  107.       this._apiAccessURL =
  108.         (URL_BASE + "/api/auth?api_key=" + FireFM.Secret.API_KEY);
  109.     }
  110.  
  111.     return this._apiAccessURL;
  112.   },
  113.  
  114.   /* Regular expression that identifies the API access URL for all localizations
  115.     of the Last.fm site. */
  116.   get URL_API_ACCESS_RE() {
  117.     if (null == this._apiAccessURLRE) {
  118.       this._apiAccessURLRE =
  119.         new RegExp(
  120.           ("^http(?:s)?://(?:[a-z]+\\.)?last(?:\\.)?fm(?:[a-z\\.]+)?/api/auth" +
  121.            "\\?api_key=" + FireFM.Secret.API_KEY),
  122.           "i");
  123.     }
  124.  
  125.     return this._apiAccessURLRE;
  126.   },
  127.  
  128.   /* Login Manager service reference. */
  129.   _loginManager : null,
  130.   /* Logger for this object. */
  131.   _logger : null,
  132.   /* The name of the currently logged in user. */
  133.   _userName : null,
  134.   /* The api session of the currently logged in user. */
  135.   _apiSession : null,
  136.   /* Preference that indicates if the login URL should point to its normal
  137.      location or the web services page where the user can enable Fire.fm use. */
  138.   _useNormalLoginPref : null,
  139.   /* Current value of the normal login URL preference. */
  140.   _useNormalLogin : false,
  141.   /* The API access approval URL. */
  142.   _apiAccessURL : null,
  143.   /* The API access approval URL regular expression. */
  144.   _apiAccessURLRE : null,
  145.  
  146.   /**
  147.    * Initializes this object.
  148.    */
  149.   init : function() {
  150.     let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
  151.     let that = this;
  152.  
  153.     this._logger = FireFM.getLogger("FireFM.Login");
  154.     this._logger.debug("init");
  155.  
  156.     this._loginManager =
  157.       Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
  158.  
  159.     this._useNormalLoginPref =
  160.       FireFM.Application.prefs.get(FireFM.PREF_BRANCH + "useNormalLoginURL");
  161.     this._useNormalLogin = this._useNormalLoginPref.value;
  162.  
  163.     // set the logged in state.
  164.     // XXX: we do this in a timeout to prevent the Master Password prompt from
  165.     // appearing before the initial Firefox window.
  166.     timer.initWithCallback(
  167.       { notify : function() {
  168.         that._checkLoggedInState();
  169.         FireFM.obsService.addObserver(that, TOPIC_COOKIE_CHANGED, false);
  170.       } },
  171.       LOGGED_IN_TIMEOUT, Ci.nsITimer.TYPE_ONE_SHOT);
  172.   },
  173.  
  174.   /**
  175.    * Gets the user name of the currently logged in user.
  176.    * @return the user name of the currently logged in user. null if no user is
  177.    * online.
  178.    */
  179.   get userName() {
  180.     this._logger.debug("[getter] userName");
  181.  
  182.     return this._userName;
  183.   },
  184.  
  185.   /**
  186.    * Get the API session key for the currently logged in user.
  187.    * @return the API session key for the currently logged in user. null if no
  188.    * user is logged in or no key is stored for this user.
  189.    */
  190.   get apiSession() {
  191.     this._logger.debug("[getter] apiSession");
  192.  
  193.     if (null != this._userName) {
  194.       if (null == this._apiSession) {
  195.         let userLower = this._userName.toLowerCase();
  196.         let loginObjs = [];
  197.         let loginCount;
  198.  
  199.         try {
  200.           loginObjs =
  201.             this._loginManager.findLogins(
  202.               {}, API_SESSION_LOGIN_HOST, null, API_SESSION_LOGIN_REALM);
  203.         } catch (e) {
  204.           this._logger.warn(
  205.             "[getter] apiSession. User rejected Master Password prompt.\n" + e);
  206.         }
  207.  
  208.         loginCount = loginObjs.length;
  209.         this._logger.debug("[getter] apiSession. Login count: " + loginCount);
  210.  
  211.         for (let i = 0; i < loginCount; i++) {
  212.           if (userLower == loginObjs[i].username.toLowerCase()) {
  213.             this._apiSession = loginObjs[i].password;
  214.             break;
  215.           }
  216.         }
  217.       }
  218.     } else {
  219.       this._apiSession = null;
  220.     }
  221.  
  222.     return this._apiSession;
  223.   },
  224.  
  225.   /**
  226.    * Set the API session key for the currently logged in user.
  227.    * @param aValue the API session key for the currently logged in user.
  228.    */
  229.   set apiSession(aValue) {
  230.     this._logger.debug("[setter] apiSession");
  231.  
  232.     let nsLoginInfo =
  233.       new Components.Constructor(
  234.         "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init");
  235.     let loginObj =
  236.       new nsLoginInfo(
  237.         API_SESSION_LOGIN_HOST, null, API_SESSION_LOGIN_REALM, this._userName,
  238.         aValue, "", "");
  239.  
  240.     try {
  241.       this._loginManager.addLogin(loginObj);
  242.     } catch (e) {
  243.       this._logger.warn(
  244.         "[setter] apiSession. User rejected Master Password prompt.\n" + e);
  245.     }
  246.  
  247.     this._apiSession = aValue;
  248.     FireFM.obsService.notifyObservers(
  249.       null, this.TOPIC_USER_AUTHENTICATION, this._userName);
  250.   },
  251.  
  252.   /**
  253.    * Checks the state of the Last.FM session cookie at startup.
  254.    */
  255.   _checkLoggedInState : function() {
  256.     this._logger.trace("_checkLoggedInState");
  257.  
  258.     let cookieManager =
  259.       Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager);
  260.     let cookies = cookieManager.enumerator;
  261.     let found = false;
  262.     let cookie;
  263.  
  264.     while (cookies.hasMoreElements()) {
  265.       cookie = cookies.getNext();
  266.  
  267.       if (this._isLastFMSessionCookie(cookie)) {
  268.         found = true;
  269.         break;
  270.       }
  271.     }
  272.  
  273.     if (found) {
  274.       this._logger.debug("_checkLoggedInState. Logged in.");
  275.       this._beginLogin();
  276.     } else {
  277.       this._logger.debug("_checkLoggedInState. Logged out.");
  278.       FireFM.obsService.notifyObservers(
  279.         null, this.TOPIC_USER_AUTHENTICATION, null);
  280.     }
  281.   },
  282.  
  283.   /**
  284.    * Indicates if the given cookie is the Last.FM session cookie, used to know
  285.    * if the user is logged in or not.
  286.    * @param aCookie the cookie to check.
  287.    * @return true if the cookie is the Last.FM session cookie, false otherwise.
  288.    */
  289.   _isLastFMSessionCookie : function(aCookie) {
  290.     // XXX: there is no logging here for performance purposes.
  291.     let isCookie =
  292.       ((aCookie instanceof Ci.nsICookie) && (".last.fm" == aCookie.host) &&
  293.        ("Session" == aCookie.name));
  294.  
  295.     return isCookie;
  296.   },
  297.  
  298.   /**
  299.    * Begins the login process for the extension.
  300.    */
  301.   _beginLogin : function() {
  302.     this._logger.trace("_beginLogin");
  303.  
  304.     let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
  305.     let that = this;
  306.  
  307.     this._userName = null;
  308.     this._apiSession = null;
  309.     // XXX: we need a timeout here for 2 reasons: (1) there are some random
  310.     // problems occurring in general right after we check the cookie, and (2)
  311.     // we experience a similar problem consistently after returning from Private
  312.     // Browsing mode.
  313.     timer.initWithCallback(
  314.       { notify : function() { that._getSession(); } }, GET_SESSION_TIMEOUT,
  315.       Ci.nsITimer.TYPE_ONE_SHOT);
  316.  
  317.     // we don't need to use the special login page for users that were already
  318.     // logged in.
  319.     if (!this._useNormalLogin) {
  320.       this._useNormalLoginPref.value = true;
  321.       this._useNormalLogin = true;
  322.     }
  323.   },
  324.  
  325.   /**
  326.    * Requests a session from Last.fm to get the currently logged in user name.
  327.    */
  328.   _getSession : function() {
  329.     this._logger.trace("_getSession");
  330.  
  331.     let that = this;
  332.     let inputStream = this._convertToStream(PARAMS_GET_SESSION);
  333.  
  334.     this._sendRequest(
  335.       URL_RPC, function(aEvent) { that._getSessionLoad(aEvent); },
  336.       function(aEvent) { that._getSessionError(aEvent); }, null,
  337.       true, inputStream);
  338.   },
  339.  
  340.   /**
  341.    * Load callback handler for the get session request.
  342.    * @param aEvent the event that triggered this function.
  343.    */
  344.   _getSessionLoad : function(aEvent) {
  345.     this._logger.trace("_getSessionLoad");
  346.  
  347.     try {
  348.       let doc = aEvent.target.responseXML;
  349.       let strings = doc.getElementsByTagName("string");
  350.       let user = strings[0].textContent;
  351.  
  352.       this._logger.debug("_getSessionLoad. User: " + user);
  353.  
  354.       if ((0 < user.length) && ("LFM_ANON" != user)) {
  355.         let apiSession;
  356.  
  357.         this._userName = user;
  358.         apiSession = this.apiSession; // _username needs to be set to do this!
  359.  
  360.         if (null == apiSession) {
  361.           let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
  362.           let that = this;
  363.  
  364.           // XXX: we need a timeout in this case because we may be in the
  365.           // middle of the login process, which may clear our notification
  366.           // due to the tab changing location.
  367.           timer.initWithCallback(
  368.             { notify : function() { that._showAPINotification(); } },
  369.             API_NOTIFICATION_TIMEOUT, Ci.nsITimer.TYPE_ONE_SHOT);
  370.         } else {
  371.           FireFM.obsService.notifyObservers(
  372.             null, this.TOPIC_USER_AUTHENTICATION, user);
  373.           // request a Scrobble session.
  374.           FireFM.Remote.scrobbleHandshake();
  375.         }
  376.       } else {
  377.         this._userName = null;
  378.         this._apiSession = null;
  379.         FireFM.obsService.notifyObservers(
  380.           null, this.TOPIC_USER_AUTHENTICATION, null);
  381.       }
  382.     } catch (e) {
  383.       this._logger.error(
  384.         "_getSessionLoad. Invalid data received: " +
  385.         aEvent.target.responseText + "\nError:\n" + e);
  386.       this._getSessionError(aEvent, aIsGetUser);
  387.     }
  388.   },
  389.  
  390.   /**
  391.    * Makes the main window display the API permission notification, if
  392.    * necessary. It will avoid doing it if the permission page is already open.
  393.    */
  394.   _showAPINotification : function() {
  395.     this._logger.trace("_showAPINotification");
  396.  
  397.     let wm =
  398.       Cc["@mozilla.org/appshell/window-mediator;1"].
  399.         getService(Ci.nsIWindowMediator);
  400.     let win = wm.getMostRecentWindow("navigator:browser");
  401.     let contentDoc = win.gBrowser.contentDocument;
  402.  
  403.     // only show this notification when not displaying the permission page. This
  404.     // can happen the first time the user clicks on the login button after
  405.     // installing.
  406.     if (!contentDoc || (this.URL_API_ACCESS != contentDoc.documentURI)) {
  407.       // tell the user to give permission to Fire.fm.
  408.       win.FireFMChrome.BrowserOverlay.showAPINotification();
  409.     }
  410.   },
  411.  
  412.   /**
  413.    * Error callback handler for the get session request.
  414.    * @param aEvent the event that triggered this function.
  415.    */
  416.   _getSessionError : function(aEvent) {
  417.     this._logger.error("_getSessionError");
  418.  
  419.     this._userName = null;
  420.     this._apiSession = null;
  421.     FireFM.obsService.notifyObservers(
  422.       null, this.TOPIC_USER_AUTHENTICATION, null);
  423.     this._defaultError("getSession", aEvent);
  424.   },
  425.  
  426.   /**
  427.    * Sends an HTTP request. This is just an utility function to save some code
  428.    * lines.
  429.    * @param aURL the url to send the request to.
  430.    * @param aLoadHandler the load callback handler. Can be null.
  431.    * @param aErrorHandler the error callback handler. Can be null.
  432.    * @param aHeaders object mapping that represents the headers to send. Can be
  433.    * null or empty.
  434.    * @param aIsPOST indicates if the method POST (true) or GET (false).
  435.    * @param aPOSTString the string or stream to send through post (optional).
  436.    */
  437.   _sendRequest : function(
  438.     aURL, aLoadHandler, aErrorHandler, aHeaders, aIsPOST, aPOSTString) {
  439.     this._logger.trace("_sendRequest");
  440.  
  441.     let request =
  442.       Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance();
  443.  
  444.     // add event handlers.
  445.     request.QueryInterface(Ci.nsIDOMEventTarget);
  446.  
  447.     if (null != aLoadHandler) {
  448.       request.addEventListener("load", aLoadHandler, false);
  449.     }
  450.  
  451.     if (null != aErrorHandler) {
  452.       request.addEventListener("error", aErrorHandler, false);
  453.     }
  454.  
  455.     // prepare and send the request.
  456.     request.QueryInterface(Ci.nsIXMLHttpRequest);
  457.     request.open((aIsPOST ? "POST" : "GET"), aURL, true);
  458.  
  459.     if (null != aHeaders) {
  460.       for (let header in aHeaders) {
  461.         request.setRequestHeader(header, aHeaders[header]);
  462.       }
  463.     }
  464.  
  465.     if (aIsPOST) {
  466.       request.send(aPOSTString);
  467.     } else {
  468.       request.send(null);
  469.     }
  470.   },
  471.  
  472.   /**
  473.    * Converts the given string into a UTF-8 string that can be sent through POST
  474.    * as if it were binary. This is required for several Last.fm calls.
  475.    * @param aString the string to convert into a stream.
  476.    * @return nsIInputStream for the given string.
  477.    */
  478.   _convertToStream : function(aString) {
  479.     this._logger.trace("_convertToStream");
  480.  
  481.     let multiStream =
  482.       Cc["@mozilla.org/io/multiplex-input-stream;1"].
  483.         createInstance(Ci.nsIMultiplexInputStream);
  484.     let converter =
  485.       Cc["@mozilla.org/intl/scriptableunicodeconverter"].
  486.         createInstance(Ci.nsIScriptableUnicodeConverter);
  487.     let inputStream;
  488.  
  489.     converter.charset = "UTF-8";
  490.     inputStream = converter.convertToInputStream(aString);
  491.     multiStream.appendStream(inputStream);
  492.  
  493.     return multiStream;
  494.   },
  495.  
  496.   /**
  497.    * Default error callback handler for the asynchronous requests.
  498.    * @param aSource a string that identifies the source of the error.
  499.    * @param aEvent the event that triggered this function.
  500.    */
  501.   _defaultError : function(aSource, aEvent) {
  502.     this._logger.debug("_defaultError");
  503.  
  504.     try {
  505.       this._logger.error(
  506.         "_defaultError. Source: " + aSource + ", status: " +
  507.         aEvent.target.status + ", response: " + aEvent.target.responseText);
  508.     } catch (e) {
  509.       this._logger.error("_defaultError. Error:\n" + e);
  510.     }
  511.   },
  512.  
  513.   /**
  514.    * Decodes a Base64 encoded string and returns the clear text version.
  515.    * @param aEncodedString a Base64 encoded string.
  516.    * @return clear text contents of the Base64 string.
  517.    * @throws Exception if the input string is badly formatted.
  518.    */
  519.   _decode : function(aEncodedString) {
  520.     this._logger.debug("_decode");
  521.  
  522.     let src = aEncodedString;
  523.     let decoded = "";
  524.     let pos = 0;
  525.     let v1, v2, v3, v4, v5, v6, v7;
  526.  
  527.     while (pos < src.length) {
  528.       v5 = BASE64.indexOf(src.charAt(pos++));
  529.       v3 = BASE64.indexOf(src.charAt(pos++));
  530.       v1 = BASE64.indexOf(src.charAt(pos++));
  531.       v2 = BASE64.indexOf(src.charAt(pos++));
  532.  
  533.       v4 = v5 << 2 | v3 >> 4;
  534.       v7 = (v3 & 15) << 4 | v1 >> 2;
  535.       v6 = (v1 & 3) << 6 | v2;
  536.       decoded += String.fromCharCode(v4);
  537.  
  538.       if (v1 != 64) {
  539.         decoded += String.fromCharCode(v7);
  540.       }
  541.  
  542.       if (v2 != 64) {
  543.         decoded += String.fromCharCode(v6);
  544.       }
  545.     }
  546.  
  547.     return  FireFM.decodeFMString(decoded);
  548.   },
  549.  
  550.   /**
  551.    * Observes notifications of cookie and track activity.
  552.    * @param aSubject The object that experienced the change.
  553.    * @param aTopic The topic being observed.
  554.    * @param aData The data related to the change.
  555.    */
  556.   observe : function(aSubject, aTopic, aData) {
  557.     // XXX: there is no logging here for performance purposes.
  558.     if (TOPIC_COOKIE_CHANGED == aTopic) {
  559.       switch (aData) {
  560.         case "added":
  561.           if (this._isLastFMSessionCookie(aSubject)) {
  562.             this._logger.debug("observe. Logged in.");
  563.             this._beginLogin();
  564.           }
  565.           break;
  566.         case "deleted":
  567.           if (this._isLastFMSessionCookie(aSubject)) {
  568.             this._logger.debug("observe. Cookie deleted.");
  569.             this._userName = null;
  570.             this._apiSession = null;
  571.             // We used to just log a user out in this case, but there seems to
  572.             // be cases where the a 'Session' cookie is removed right after a
  573.             // new one has been set.
  574.             this._checkLoggedInState();
  575.           }
  576.           break;
  577.         case "cleared":
  578.         case "reload":
  579.           this._logger.info("observe. Cookie list reset.");
  580.           this._userName = null;
  581.           this._apiSession = null;
  582.           this._checkLoggedInState();
  583.           break;
  584.       }
  585.     }
  586.   }
  587. };
  588.  
  589. /**
  590.  * FireFM.Login constructor.
  591.  */
  592. (function() {
  593.   this.init();
  594. }).apply(FireFM.Login);
  595.